/*
 * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License
 *       at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jsyntaxpane.components;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BorderFactory;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import jsyntaxpane.SyntaxDocument;
import jsyntaxpane.SyntaxView;
import jsyntaxpane.actions.ActionUtils;
import jsyntaxpane.actions.gui.GotoLineDialog;
import jsyntaxpane.util.Configuration;

/**
 * This class will display line numbers for a related text component. The text
 * component must use the same line height for each line.
 *
 * This class was designed to be used as a component added to the row header of
 * a JScrollPane.
 *
 * Original code from
 * http://tips4java.wordpress.com/2009/05/23/text-component-line-number/
 *
 * @author Rob Camick
 *
 * Revised for jsyntaxpane
 *
 * @author Ayman Al-Sairafi
 */
public class LineNumbersRuler extends JPanel
        implements CaretListener, DocumentListener, PropertyChangeListener, SyntaxComponent {

    public static final String PROPERTY_BACKGROUND = "LineNumbers.Background";
    public static final String PROPERTY_FOREGROUND = "LineNumbers.Foreground";
    public static final String PROPERTY_CURRENT_BACK = "LineNumbers.CurrentBack";
    public static final String PROPERTY_LEFT_MARGIN = "LineNumbers.LeftMargin";
    public static final String PROPERTY_RIGHT_MARGIN = "LineNumbers.RightMargin";
    public static final String PROPERTY_Y_OFFSET = "LineNumbers.YOFFset";
    public static final int DEFAULT_R_MARGIN = 5;
    public static final int DEFAULT_L_MARGIN = 5;
    private Status status;
    private final static int HEIGHT = Integer.MAX_VALUE - 1000000;
    //  Text component this TextTextLineNumber component is in sync with
    protected JEditorPane editor; //JPEXS: changed to protected
    private int minimumDisplayDigits = 2;
    //  Keep history information to reduce the number of times the component
    //  needs to be repainted
    private int lastDigits;
    private int lastHeight;
    private int lastLine;
    //JPEXS: private lowered to protected to allow replacing mouseListener
    protected MouseListener mouseListener = null;
    // The formatting to use for displaying numbers.  Use in String.format(numbersFormat, line)
    private String numbersFormat = "%3d";

    private Color currentLineColor;

    /**
     * Get the JscrollPane that contains this EditorPane, or null if no
     * JScrollPane is the parent of this editor
     *
     * @param editorPane
     * @return
     */
    public JScrollPane getScrollPane(JTextComponent editorPane) {
        Container p = editorPane.getParent();
        while (p != null) {
            if (p instanceof JScrollPane) {
                return (JScrollPane) p;
            }
            p = p.getParent();
        }
        return null;
    }

    @Override
    public void config(Configuration config) {
        int right = config.getInteger(PROPERTY_RIGHT_MARGIN, DEFAULT_R_MARGIN);
        int left = config.getInteger(PROPERTY_LEFT_MARGIN, DEFAULT_L_MARGIN);
        Color foreground = config.getColor(PROPERTY_FOREGROUND, Color.BLACK);
        setForeground(foreground);
        Color back = config.getColor(PROPERTY_BACKGROUND, Color.WHITE);
        setBackground(back);
        setBorder(BorderFactory.createEmptyBorder(0, left, 0, right));
        currentLineColor = config.getColor(PROPERTY_CURRENT_BACK, back);
    }

    @Override
    public void install(final JEditorPane editor) {
        this.editor = editor;

        setFont(editor.getFont());

        // setMinimumDisplayDigits(3);
        editor.getDocument().addDocumentListener(this);
        editor.addCaretListener(this);
        editor.addPropertyChangeListener(this);
        JScrollPane sp = getScrollPane(editor);
        sp.setRowHeaderView(this);
        mouseListener = new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                GotoLineDialog.showForEditor(editor);
            }
        };
        addMouseListener(mouseListener);
        status = Status.INSTALLING;
    }

    @Override
    public void deinstall(JEditorPane editor) {
        removeMouseListener(mouseListener);
        status = Status.DEINSTALLING;
        editor.getDocument().removeDocumentListener(this);
        editor.removeCaretListener(this);
        editor.removePropertyChangeListener(this);
        JScrollPane sp = getScrollPane(editor);
        if (sp != null) {
            sp.setRowHeaderView(null);
        }
    }

    /**
     * Gets the minimum display digits
     *
     * @return the minimum display digits
     */
    public int getMinimumDisplayDigits() {
        return minimumDisplayDigits;
    }

    /**
     * Specify the minimum number of digits used to calculate the preferred
     * width of the component. Default is 3.
     *
     * @param minimumDisplayDigits the number digits used in the preferred width
     * calculation
     */
    public void setMinimumDisplayDigits(int minimumDisplayDigits) {
        this.minimumDisplayDigits = minimumDisplayDigits;
        setPreferredWidth();
    }

    /**
     * Calculate the width needed to display the maximum line number
     */
    private void setPreferredWidth() {
        int lines = ActionUtils.getLineCount(editor);
        int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);

        //  Update sizes when number of digits in the line number changes
        if (lastDigits != digits) {
            lastDigits = digits;
            numbersFormat = "%" + digits + "d";
            FontMetrics fontMetrics = getFontMetrics(getFont());
            int width = fontMetrics.charWidth('0') * digits;
            Insets insets = getInsets();
            int preferredWidth = insets.left + insets.right + width;

            Dimension d = getPreferredSize();
            d.setSize(preferredWidth, HEIGHT);
            setPreferredSize(d);
            setSize(d);

        }
    }

    /**
     * Draw the line numbers
     */
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        FontMetrics fontMetrics = editor.getFontMetrics(editor.getFont());
        Insets insets = getInsets();
        int currentLine = -1;
        try {
            // get current line, and add one as we start from 1 for the display
            currentLine = ActionUtils.getLineNumber(editor, editor.getCaretPosition()) + 1;
        } catch (BadLocationException ex) {
            // this wont happen, even if it does, we can ignore it and we will not have
            // a current line to worry about...
        }

        int lh = fontMetrics.getHeight();
        int maxLines = ActionUtils.getLineCount(editor);
        SyntaxView.setRenderingHits((Graphics2D) g);

        Rectangle bounds = g.getClipBounds();
        int minY = bounds.y;
        int maxY = minY + bounds.height;
        for (int line = 1; line <= maxLines; line++) {
            int y = line * lh;
            if (y < minY) {
                continue;
            }
            if (y - lh > maxY) {
                break;
            }
            String lineNumber = String.format(numbersFormat, line);
            if (line == currentLine) {
                g.setColor(currentLineColor);
                g.fillRect(0, y - lh + fontMetrics.getDescent() - 1, getWidth(), lh);
                g.setColor(getForeground());
                g.drawString(lineNumber, insets.left, y);
            } else {
                g.drawString(lineNumber, insets.left, y);
            }
        }
    }

//
//  Implement CaretListener interface
//
    @Override
    public void caretUpdate(CaretEvent e) {
        //  Get the line the caret is positioned on

        int caretPosition = editor.getCaretPosition();
        Element root = editor.getDocument().getDefaultRootElement();
        int currentLine = root.getElementIndex(caretPosition);

        //  Need to repaint so the correct line number can be highlighted
        if (lastLine != currentLine) {
            repaint();
            lastLine = currentLine;
        }
    }

//
//  Implement DocumentListener interface
//
    @Override
    public void changedUpdate(DocumentEvent e) {
        documentChanged();
    }

    @Override
    public void insertUpdate(DocumentEvent e) {
        documentChanged();
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        documentChanged();
    }

    /*
     *  A document change may affect the number of displayed lines of text.
     *  Therefore the lines numbers will also change.
     */
    private void documentChanged() {
        //  Preferred size of the component has not been updated at the time
        //  the DocumentEvent is fired

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                int preferredHeight = editor.getPreferredSize().height;

                //  Document change has caused a change in the number of lines.
                //  Repaint to reflect the new line numbers
                if (lastHeight != preferredHeight) {
                    setPreferredWidth();
                    repaint();
                    lastHeight = preferredHeight;
                }
            }
        });
    }

    /**
     * Implement PropertyChangeListener interface
     */
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals("document")) {
            if (evt.getOldValue() instanceof SyntaxDocument) {
                SyntaxDocument syntaxDocument = (SyntaxDocument) evt.getOldValue();
                syntaxDocument.removeDocumentListener(this);
            }
            if (evt.getNewValue() instanceof SyntaxDocument && status.equals(Status.INSTALLING)) {
                SyntaxDocument syntaxDocument = (SyntaxDocument) evt.getNewValue();
                syntaxDocument.addDocumentListener(this);
                setPreferredWidth();
                repaint();
            }
        } else if (evt.getNewValue() instanceof Font) {
            setPreferredWidth();
            repaint();
        }
    }
}
